<?php
namespace WDB\WebUI;
use WDB;
/**
 * class for handling URIs with PHP query strings
 *
 * @author Richard Ejem <richard(at)ejem.cz>
 * @package WDB
 */
class Uri {

    /**@var array $parameters*/
    public $params = array(); //standard ?key=value PHP query part

    /**@var string $fragment*/
    public $fragment = ''; // # URI fragment

    /**@var string $path*/
    public $path; //link root (index.php, ./ ...)

    /**@var string $abspath_prefix uri absolute path prefix if the path is relative (whole uri is protocol :// server[:port] abspath_prefix abspath ? query # fragment*/
    public $abspath_prefix; //link root (index.php, ./ ...)

    /**@var string $host */
    public $host;

    /**@var string $protocol most likely "http" or "https"*/
    public $protocol;

    /**@var int $port*/
    public $port;

    /**@var int $urlmode 1=relative,2=absolute path,3 = absolute uri */
    private $urlmode;

    /**@var string $title saved uri title - not used by this class, useful for naming URIs or storing link text*/
    public $title;

    /**
     * @param string $root URI root (absolute or relative), may also include query - will be automatically parsed
     * @param array $params query parameters
     * @param string $fragment URI fragment after #
     */
    public function __construct($root = './', $params = array(), $fragment = NULL) {
        $this->parseUri($root,$params,$fragment);
    }

    /**
     * loads URI from a string.
     *
     * @param string $root raw uri
     * @param array $params
     * @param string $fragment
     * @return \WDB\WebUI\Uri
     */
    public function parseUri($root = './', $params = array(), $fragment = NULL)
    {
        if ($root == '') $root = '.';
        $root = $this->parseQuery($root);
        $this->addParams($params);
        if ($fragment != NULL)
        {
            $this->fragment = $fragment;
        }
        $data = explode('://',$root,2);
        //absolute uri entered
        if (count($data) > 1)
        {
            $this->urlmode = 3;
            //cut protocol
            $this->protocol = strtolower(preg_replace('~[^a-z0-9]~i', '', $data[0]));
            //cut host domain
            preg_match('~^[a-z0-9-.]+~i', $data[1], $matches);
            $this->host = $matches[0];
            $data = substr($data[1], strlen($this->host));
            //cut port if present
            if ($data[0] == ':')
            {
                $data = substr($data, 1);
                preg_match('~^[0-9]+~i', $data, $matches);
                $this->port = $matches[0];
                $data = substr($data, strlen($this->port));
            }
            else
            {
                $this->port = NULL;
            }
            $root = $data;
        }
        else
        {
            $this->protocol = (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') ? 'https' : 'http';
            $this->host = $_SERVER['HTTP_HOST'];
            $this->port = NULL;
        }
        $this->path = $root;
        if ($this->path[0] == '/')
        { //absolute path
            if (!$this->urlmode) $this->urlmode = 2;
            $this->abspath_prefix = '';
        }
        else
        { //relative path
            if (!$this->urlmode) $this->urlmode = 1;
            //extract current relative path context
            $this->abspath_prefix = preg_replace('~[^/]*$~', '', $_SERVER['PHP_SELF']);
        }
        return $this;
    }

    /**
     * @param string $key
     * @param string $value
     * @return \WDB\WebUI\Uri
     */
    public function addParam($key, $value) {
        $this->_addParam($key, $value, $this->params);
        return $this;
    }
    
    private function _addParam($key, $value, &$container)
    {
        $ct = &$container;
        while (preg_match('~([^[]*)\\[(.*?)](.*)~', $key, $matches))
        {
            if (strlen($matches[3]) > 0 && $matches[3]{0} != '[') $matches[3] = ''; //baz[baz] is ignored by php when key is foo[bar]baz[baz]
            $key=$matches[2].$matches[3];
            if (!isset($container[$matches[1]])) $container[$matches[1]] = array();
            
            //workaround to update ct reference to its own element
            $ctx = &$ct[$matches[1]];
            unset($ct);
            $ct = &$ctx;
        }
        if (WDB\Utils\Arrays::isArrayAccessible($value))
        {
            $this->_addParams(array($key => $value), $ct);
        }
        else
        {
            $ct[$key] = $value;
        }
    }
    
    /**
     * @param string $key
     * @return string
     */
    public function getParam($key) {
        return isset($this->params[$key]) ? $this->params[$key] : NULL;
    }
    
    private function _addParams($params, &$container)
    {
        foreach ($params as $key=>$value)
        {
            if (WDB\Utils\Arrays::isArrayAccessible($value))
            {
                if (!isset($this->params[$key])) $this->params[$key] = array();
                $this->_addParams($value, $container[$key]);
            }
            else
            {
                $this->_addParam($key, $value, $container);
            }
        }
    }
    
    /**
     * @param array $params associative parameter array
     * @return \WDB\WebUI\Uri
     */
    public function addParams($params) {
        $this->_addParams($params, $this->params);
        return $this;
    }
    /**
     * @param string|array $key parameter or list of parameters
     * @return \WDB\WebUI\Uri
     */
    public function removeParams($key) {
        if (is_array($key))
        {
            foreach ($key as $k) $this->removeParams($k);
        }
        else
        {
            if (isset($this->params[$key])) unset($this->params[$key]);
        }
        return $this;
    }
    /**
     *
     * @param array $keys list of allowed keys
     * @return \WDB\WebUI\Uri
     */
    public function restrictParams($keys) {
        if (!is_array($keys))
        { //arguments are list of non-iterable object (supposedly list of parameters itself)
            $keys = func_get_args();
        }
        foreach ($this->params as $key=>$val)
        {
            if (!in_array($key,$keys)) unset($this->params[$key]);
        }
        return $this;
    }
    /**
     * Clones object to fetch relative URIs. Returns self to enable chaining.
     *
     * @return \WDB\WebUI\Uri
     */
    public function relative()
    {
        $copy = clone $this;
        $copy->urlmode = 1;
        return $copy;
    }
    /**
     * Configures object to fetch absolute resource path (host and protocol not included). Returns self to enable chaining.
     * 
     * @return \WDB\WebUI\Uri
     */
    public function absolute_path()
    {
        $copy = clone $this;
        $copy->urlmode = 2;
        return $copy;
    }
    /**
     * Configures object to fetch full absolute URIs. Returns self to enable chaining.
     *
     * @return \WDB\WebUI\Uri
     */
    public function absolute()
    {
        $copy = clone $this;
        $copy->urlmode = 3;
        return $copy;
    }
    
    private $fetch_params_firstpass;
    
    private function fetch_params(array $params, $prefix = '')
    {
        $link = '';
        foreach ($params as $key=>$val) {
            if ($val === NULL) continue;
            $full_key = $prefix == '' ? urlencode($key) : $prefix.'['.urlencode($key).']';
            $full_key = str_replace(array('%5B', '%5D'), array('[', ']'), $full_key);
            if (is_array($val))
            {
                $link .= $this->fetch_params($val, $full_key); 
            }
            else
            {
                if ($this->fetch_params_firstpass) {
                    $this->fetch_params_firstpass = FALSE;
                } else {
                    $link .= '&';
                }
                $link .= $full_key.'='.urlencode($val);
            }
        }
        return $link;
    }
    
    /**
     * @param array $params additional parameters
     * @param string $fragment
     * @param bool $deleteParams fetch uri with cleaned all parameters
     * @return void
     */
    public function redirect($params = array(), $fragment = NULL, $deleteParams = FALSE)
    {
        header('location: '.$this->absolute()->fetch($params, $fragment, $deleteParams));
        die();
    }
    
    /**
     * @param array $params additional parameters
     * @param string $fragment
     * @param bool $deleteParams fetch uri with cleaned all parameters
     * @return string
     */
    public function fetch($params = array(), $fragment = NULL, $deleteParams = FALSE) { //create URI
        $oldparams = $this->params;
        if ($deleteParams) $this->params = array();
        $this->addParams($params);
        switch ($this->urlmode)
        {
        case 2:
            $link = $this->abspath_prefix;
            break;
        case 3:
            $link = $this->protocol.'://'.$this->host;
            if ($this->port && !
                    (($this->protocol == 'http' && $this->port == 80) ||
                     ($this->protocol == 'https' && $this->port == 443)
                            ))
            {
                $link .= ':'.$this->port;
            }
            $link .= $this->abspath_prefix;
            break;
        case 1:
        default:
            $link = '';
            break;
        }
        $link .= $this->path.((count($this->params) > 0) ? '?' : '');
        $this->fetch_params_firstpass = TRUE;
        $link .= $this->fetch_params($this->params);
        if ($fragment !== NULL) {
            if ($fragment != '')
            {
                $link .= '#'.urlencode($fragment);
            }
        } elseif (!empty($this->fragment)) {
            $link .= '#'.urlencode($this->fragment);
        }
        $this->params = $oldparams;
        return $link;
    }
    public function  __toString() {
        return $this->fetch();
    }
    /**
     * parses query and fragment part of uri
     *
     * @param string $uri raw uri
     * @return \WDB\WebUI\Uri
     */
    private function parseQuery($uri) {
        $this->params = array();
        $this->fragment = NULL;
        $query = NULL;
        $fragmentPosition = strpos($uri, '#');
        if ($fragmentPosition === FALSE) { //-fragment
            $queryPosition = strpos($uri, '?');
            if ($queryPosition === FALSE) { //-fragment, -query
                $root = $uri; //root set
            } else { //-fragment, +query
                list($root, $query) = explode('?', $uri); //root and uri parsed
            }
        } else { //+fragment
            list($root, $this->fragment) = explode('#', $uri); //fragment parsed
            $queryPosition = strpos($root, '?');
            if ($queryPosition !== FALSE) { //+fragment, +query                
                list($root, $query) = explode('?', $root); //root and query parsed
            }
        }
        if ($query)
        {
            $query = explode('&', $query);
            foreach ($query as $param) {
                $kv = explode('=', $param);
                if ($kv[0] == '') continue;
                if (count($kv) > 1) {
                    $this->addParam(urldecode($kv[0]), urldecode($kv[1]));
                } else {
                    $this->addParam(urldecode($kv[0]), TRUE);
                }
            }
        }
        return $root;
    }
}